package sqlx

import (
	"context"
	"fmt"
	"gitee.com/brookscoder/awesome-mall-go/common/log"
	"gitee.com/brookscoder/awesome-mall-go/model"
	_ "github.com/go-sql-driver/mysql"
	"github.com/jmoiron/sqlx"
	"go.uber.org/zap"
	"math/rand"
	"strconv"
	"time"
)

// 实例化数据库对象
var Db *sqlx.DB

// InitDB 初始化数据库(需要先在docker中启动db镜像并建立malldb的数据源)
func InitDB() (err error) {
	// DSN: Data Source Name e.g. "user:password@tcp(127.0.0.1:3306)/testdb"
	dsn := "root:123456@tcp(127.0.0.1:3306)/malldb"
	// 初始化数据库（不会校验账号密码）
	Db, err = sqlx.Connect("mysql", dsn)
	if err != nil {
		return err
	}
	// 尝试与数据库建立连接(校验dsn是否正确)
	err = Db.Ping()
	if err != nil {
		return err
	} else {
		// 设置与数据库建立连接的最大数目
		Db.SetMaxOpenConns(100)
		// 设置与数据库连接的最大闲置数
		Db.SetMaxIdleConns(50)
	}
	return nil
}

// TestDataBase 学习数据库sqlx框架的CRUD实验方法
func TestDataBase() {
	//rand.Seed(time.Now().UnixNano())
	//testQueryOneUser()
	//testAddOneUser("测试", "机器人"+strconv.Itoa(rand.Intn(100)), createRandomPhoneNum())
	//testQueryUserList()
	//testUpdateOneUser()
	//testDeleteOneUser()
	//testQueryUserListByPepare()
	//testTransActionDemo()
}

//---------- Test DataBase  Method ----------
/*测试查询单条记录*/
func testQueryOneUser() {
	sqlStr := "select id, first_name, last_name, age from user_info where id=?"
	var u model.User
	err := Db.Get(&u, sqlStr, 1)
	if err != nil {
		log.Warning(context.Background(), "查询单条信息失败", zap.Error(err))
		return
	}

	fmt.Println("查询单条信息成功 %s", u)
	log.Info(context.Background(), "查询单条信息成功", zap.Any("用户信息", u))
}

/*测试添加一个用户*/
func testAddOneUser(firstName string, lastName string, phoneNumber string) bool {
	sqlStr := "insert into user_info(first_name, last_name, phone, sex) values (?,?,?,0)"
	ret, err := Db.Exec(sqlStr, firstName, lastName, phoneNumber)
	if err != nil {
		log.Warning(context.Background(), "插入单条信息失败", zap.Error(err))
		return false
	}

	theID, err := ret.LastInsertId() // 新插入数据的id
	if err != nil {
		log.Warning(context.Background(), "获取最后插入单条信息失败", zap.Error(err))
		return false
	}

	log.Info(context.Background(), "插入单条信息成功", zap.Any("用户信息", theID))
	return true
}

/*查询整个用户列表*/
func testQueryUserList() {
	sqlStr := "select id, first_name, last_name, phone, age from user_info where id > ?"
	rows, err := Db.Query(sqlStr, 0)
	if err != nil {
		log.Warning(context.Background(), "查询用户列表失败", zap.Error(err))
		return
	}
	// 关闭rows释放持有的数据库连接（非常重要）;defer是在函数执行完成后return前执行，无关写在哪个位置
	defer rows.Close()

	// 循环读取结果集中的数据
	for rows.Next() {
		var u model.User
		err := rows.Scan(&u.Id, &u.FirstName, &u.LastName, &u.MobilePhone, &u.Age)
		if err != nil {
			log.Warning(context.Background(), "用户列表赋值失败", zap.Error(err))
			return
		}
		log.Info(context.Background(), "用户列表赋值成功", zap.Any("用户信息", u))
	}
}

/*查询整个用户列表(带预处理)*/
func testQueryUserListByPepare() {
	sqlStr := "select id, first_name, last_name, phone, age from user_info where id > ?"
	stmt, err := Db.Prepare(sqlStr) //预处理带占位符的sql语句
	if err != nil {
		log.Warning(context.Background(), "查询用户列表失败(预处理)", zap.Error(err))
		return
	}
	// 关闭stmt释放持有的连接
	defer stmt.Close()
	rows, err := stmt.Query(0)
	if err != nil {
		log.Warning(context.Background(), "查询用户列表失败(预处理)", zap.Error(err))
		return
	}
	// 关闭rows释放持有的数据库连接（非常重要）;defer是在函数执行完成后return前执行，无关写在哪个位置
	defer rows.Close()

	// 循环读取结果集中的数据
	for rows.Next() {
		var u model.User
		err := rows.Scan(&u.Id, &u.FirstName, &u.LastName, &u.MobilePhone, &u.Age)
		if err != nil {
			log.Warning(context.Background(), "用户列表赋值失败(预处理)", zap.Error(err))
			return
		}
		log.Info(context.Background(), "用户列表赋值成功(预处理)", zap.Any("用户信息", u))
	}
}

/*测试更新单个用户*/
func testUpdateOneUser() {
	sqlStr := "update user_info set age=? where id = ?"
	ret, err := Db.Exec(sqlStr, 39, 3)
	if err != nil {
		log.Warning(context.Background(), "更新单条用户信息失败", zap.Error(err))
		return
	}

	rows, err := ret.RowsAffected() //操作影响的行数
	if err != nil {
		log.Warning(context.Background(), "更新单条用户信息失败", zap.Error(err))
		return
	}
	log.Info(context.Background(), "更新单条用户信息成功", zap.Int64("更新的行数目", rows))
}

/*测试删除单个用户*/
func testDeleteOneUser() {
	sqlStr := "delete from user_info where first_name = ?"
	ret, err := Db.Exec(sqlStr, "李")
	if err != nil {
		log.Warning(context.Background(), "删除单条用户信息失败", zap.Error(err))
		return
	}
	rows, err := ret.RowsAffected() //操作影响的行数
	if err != nil {
		log.Warning(context.Background(), "删除单条用户信息失败", zap.Error(err))
		return
	}
	log.Info(context.Background(), "删除单条用户信息成功", zap.Int64("更新的行数目", rows))
}

/*测试操作一组事务（全部操作要么同时成功，要么同时失败，不存在中间态）*/
func testTransActionDemo() {
	/*
		## 什么是事务？

		事务：一个最小的不可再分的工作单元；通常一个事务对应一个完整的业务(例如银行账户转账业务，该业务就是一个最小的工作单元)，同时这个完整的业务需要执行多次的DML(insert、update、delete)语句共同联合完成。A转账给B，这里面就需要执行两次update操作。

		在MySQL中只有使用了Innodb数据库引擎的数据库或表才支持事务。事务处理可以用来维护数据库的完整性，保证成批的SQL语句要么全部执行，要么全部不执行。

		## 事务的ACID

		通常事务必须满足4个条件（ACID）：原子性（Atomicity，或称不可分割性）、一致性（Consistency）、隔离性（Isolation，又称独立性）、持久性（Durability）。

		|  条件   |  解释   |
		| :--: | :--: |
		|  原子性   |  一个事务（transaction）中的所有操作，要么全部完成，要么全部不完成，不会结束在中间某个环节。事务在执行过程中发生错误，会被回滚（Rollback）到事务开始前的状态，就像这个事务从来没有执行过一样。   |
		|  一致性   |  在事务开始之前和事务结束以后，数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则，这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。   |
		|  隔离性   |  数据库允许多个并发事务同时对其数据进行读写和修改的能力，隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别，包括读未提交（Read uncommitted）、读提交（read committed）、可重复读（repeatable read）和串行化（Serializable）。   |
		|  持久性   |  事务处理结束后，对数据的修改就是永久的，即便系统故障也不会丢失。   |
	*/

	// 开始事务（BeginX替代原生方法里的Begin）
	tx, err := Db.Beginx()
	if err != nil {
		if tx != nil {
			tx.Rollback() //回滚
		}
		log.Warning(context.Background(), "开启事务失败", zap.Error(err))
		return
	}
	// 部署事务（MustExec替代原生方法里的Exec）
	sqlStr1 := "Update user_info set sex=8 where first_name=?"
	_ = tx.MustExec(sqlStr1, "李")
	sqlStr2 := "Update user_info set sex=9 where first_name=?"
	_ = tx.MustExec(sqlStr2, "王")

	// 提交事务
	err = tx.Commit()
	if err != nil {
		tx.Rollback() //回滚
		log.Warning(context.Background(), "提交事务失败", zap.Error(err))
		return
	}

	log.Info(context.Background(), "执行事务成功", zap.Error(err))

}

//------- Private  Method ----------
/*生成一个随机11位手机号*/
func createRandomPhoneNum() string {
	rand.Seed(time.Now().UnixNano())
	phoneNum := (rand.Intn(100000000) + 19900000000)
	println("随机手机号 %s", phoneNum)
	r := strconv.Itoa(phoneNum)
	return r
}
